Skip to content

Fix GH-20875: Propagate IN_GET guard in get_property_ptr_ptr for lazy proxies#21463

Open
iliaal wants to merge 3 commits intophp:masterfrom
iliaal:fix/gh-20875-lazy-proxy-guard-propagation
Open

Fix GH-20875: Propagate IN_GET guard in get_property_ptr_ptr for lazy proxies#21463
iliaal wants to merge 3 commits intophp:masterfrom
iliaal:fix/gh-20875-lazy-proxy-guard-propagation

Conversation

@iliaal
Copy link
Contributor

@iliaal iliaal commented Mar 17, 2026

Summary

  • zend_std_get_property_ptr_ptr() was the only property handler that did not propagate the IN_GET guard to the underlying object when forwarding from a lazy proxy after initialization
  • Without the guard, __get is called on the underlying object when it shouldn't be, causing assertion failures (_get_zval_ptr_tmp, _zendi_try_convert_scalar_to_number, ZEND_RETURN_BY_REF_SPEC_VAR_HANDLER)
  • The same guard-copying pattern already existed in read_property, write_property, unset_property, and has_property since commit 26f5009 (Fix lazy proxy calling magic methods twice #18039)

Fixes #20875, #20873, #20854.

…azy proxies

zend_std_get_property_ptr_ptr() was the only property handler that did
not propagate the IN_GET guard to the underlying object when forwarding
from a lazy proxy after initialization. This caused __get to be called
on the underlying object when it shouldn't be, leading to assertion
failures.

The same guard-copying pattern already existed in read_property,
write_property, unset_property, and has_property since commit
26f5009 (phpGH-18039).

Also fixes phpGH-20873 and phpGH-20854.

Closes phpGH-20875
Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this doesn't solve the problem I described in GH-20854 fully.

--TEST--
Test
--FILE--
<?php

class Foo {
    public $_;

    public function __get($name) {
        global $proxy;
        printf("__get(\$%s) on %d\n", $name, spl_object_id($this));
        return $proxy->{$name};
    }
}

$proxy = (new ReflectionClass(Foo::class))->newLazyProxy(function () {
    echo "Init\n";

    global $real;
    $real = new Foo();
    return $real;
});

$proxy->_;
$real->x;

?>
--EXPECTF--
Init
__get($x) on 1
__get($x) on 3

Warning: Undefined property: Foo::$x in %s on line %d

This calls __get twice, once on the real object and then on the proxy. The issue here is that the forwarded guard check is skipped if the initial guard on the proxy already fails. I'll leave it up to @arnaud-lb whether that's worth fixing.

Move the zend_lazy_object_must_init check in the dynamic property
branch of get_property_ptr_ptr above the deprecation/error handling,
so that a lazy proxy delegates to the real instance before emitting
deprecation notices. This eliminates duplicate deprecation warnings
that fired once on the proxy and once on the underlying object.
@iliaal iliaal requested a review from iluuu1994 March 18, 2026 17:08
return retval;
}
}
if (UNEXPECTED(zend_lazy_object_must_init(zobj))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code needs to stay within the if (EXPECTED(!zobj->ce->__get) || UNEXPECTED((*zend_get_property_guard(zobj, name)) & IN_GET)) {, otherwise the lazy object is initialized even when __get is called. Test case:

--TEST--
Test
--FILE--
<?php

class Foo {
    public $_;
    
    public function &__get($name) {
        echo "__get\n";
        return $name;
    }
}

$proxy = (new ReflectionClass(Foo::class))->newLazyProxy(function () {
    echo "init\n";
    return new Foo();
});
$x = &$proxy->x;

?>
--EXPECT--
__get

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the lazy init back inside the __get guard, just above the deprecation checks. Added your test case as gh20875_proxy_get_no_init.phpt.

The previous commit hoisted the lazy init check above the __get guard
conditional, which caused proxy initialization even when __get should
handle the property access. Move the check back inside the guard but
before the deprecation/error handling so dynamic property deprecation
still only fires once (on the instance, not the proxy).

Add regression test for proxy-with-__get not initializing on dynamic
property access (from Ilija's review).
@iluuu1994 iluuu1994 requested a review from arnaud-lb March 18, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Assertion failure in _get_zval_ptr_tmp with lazy proxy

2 participants